<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">

  <meta property="og:image:height" content="419">
  <meta property="og:image:width" content="800">
  <meta property="og:title" content="TWGL.js - GPGPU Particles">
  <meta property="og:description" content="This example shows how to use GPGPU  in WebGL with the twgl.js library.">
  <meta property="og:url" content="http://twgljs.org/examples/gpgpu-particles.html">
  <meta property="og:image" content="http://twgljs.org/examples/screenshots/gpgpu-particles.png">

  <link href="/resources/images/twgljs-icon.png" rel="shortcut icon" type="image/png">
  <title>GPGPU Particles.</title>
</head>
<body style="margin:0; font-family: monospace;">

<canvas id="canvasgl" style="height: 100vh; width: 100vw; display: block;"></canvas>
<div id="b" style="position: absolute; top: 10px; width: 100%; text-align: center; z-index: 2;"><a href="http://twgljs.org">twgl.js</a> - GPGPU Particles.</div>

<script src="../dist/4.x/twgl.min.js"></script>
<script>
  'use strict';

  // particle initialization
  const vInit = `
    precision mediump float;
    attribute vec2 position;
    varying vec2 v_position;
    
    void main() {
      v_position = 0.5 * position + 0.5;
      gl_Position = vec4(position, 0.0, 1.0);
    }`;
  const fInit = `
    precision mediump float;
    varying vec2 v_position;

    void main() {
      gl_FragColor = vec4(v_position, 0.0, 0.0);
    }`;

  // particle physics in a cycle
  const vPhysics = `
    precision mediump float;

    attribute vec2 position;
    varying vec2 v_texcoord;
    
    void main() {
      v_texcoord = 0.5 * position + 0.5;
      gl_Position = vec4(position, 0.0, 1.0);
    }`;
  const fPhysics = `
    precision mediump float;

    varying vec2 v_texcoord;
    uniform sampler2D u_texture;
    uniform vec2 gravity_center;
    uniform float off_gravity;
    uniform float restore_colors;
    uniform float dt;
    
    vec2 n2rand() {
      return vec2(fract(sin(dot(v_texcoord.xy, vec2(12.9898, 78.233))) * 43758.5453),
        fract(sin(dot(v_texcoord.xy * 1.61803, vec2(12.9898, 78.233))) * 43758.5453));
    }

    void main() {
      vec4 particle = texture2D(u_texture, v_texcoord);
      vec2 r, d, a, x, v;

      x = particle.xy;
      v = particle.zw;
      r = x - gravity_center;
      d = x - v_texcoord;

      // physics emulation
      a = (1.0 - off_gravity) * ( -normalize(r) * dt * clamp(3. / dot(r, r), 0., 2.) + 0.03 * (n2rand() - 0.5) );
      x += (1.0 - restore_colors) * (v - 0.5) * dt;
      x += restore_colors * -d;
      v += a;

      // mirror edge
      v -= 2.0 * (v - 0.5) * step(0.0, abs(x - 0.5) - 0.5);
      x = abs(abs(abs(x) - 1.0) - 1.0);

      gl_FragColor = vec4(x, v);
    }`;

  // drawing particles
  const vDraw = `
    precision mediump float;

    attribute vec2 v_texcoord;
    uniform sampler2D u_texture;
    varying vec3 color;   

    vec3 hsv2rgb(vec3 c) {
      vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
      vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
      return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
    }

    void main () {
      vec4 particle = texture2D(u_texture, v_texcoord);
      color = hsv2rgb(vec3(0.5 * v_texcoord + 0.4, 0.9));
      gl_Position = vec4(particle.xy * 2.0 - 1.0, 0.0, 1.0);
      gl_PointSize = 1.0;
    }`;
  const fDraw = `
    precision mediump float;
    varying vec3 color;

    void main () {
      gl_FragColor = vec4(color, 1.);
    }`;

  const mousepos = [0.5, 0.5];
  const canvas = document.getElementById('canvasgl');
  const gl = twgl.getContext(canvas, { depth: false, antialiasing: false });

  const programInit = twgl.createProgramInfo(gl, [vInit, fInit]);
  const programPhysics = twgl.createProgramInfo(gl, [vPhysics, fPhysics]);
  const programDraw = twgl.createProgramInfo(gl, [vDraw, fDraw]);

  const n = 256;
  const m = 256;
  let fb1 = twgl.createFramebufferInfo(gl, undefined, n, m);
  let fb2 = twgl.createFramebufferInfo(gl, undefined, n, m);
  const positionObject = { position: { data: [1, 1, 1, -1, -1, -1, -1, 1], numComponents: 2 } };
  const positionBuffer = twgl.createBufferInfoFromArrays(gl, positionObject);

  const pointData = [];
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < m; j++) {
      pointData.push(i / (n - 1));
      pointData.push(j / (m - 1));
    }
  }
  const pointsObject = { v_texcoord: { data: pointData, numComponents: 2 } };
  const pointsBuffer = twgl.createBufferInfoFromArrays(gl, pointsObject);

  // particle initialization
  gl.useProgram(programInit.program);
  twgl.setBuffersAndAttributes(gl, programInit, positionBuffer);
  twgl.bindFramebufferInfo(gl, fb1);
  twgl.drawBufferInfo(gl, positionBuffer, gl.TRIANGLE_FAN);

  let dt;
  let prevTime;
  let temp;
  let offGravity = 0;
  let restoreColors = 0;

  function draw(time) {
    twgl.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    dt = (prevTime) ? time - prevTime : 0;
    prevTime = time;

    // particle physics
    gl.useProgram(programPhysics.program);
    twgl.setBuffersAndAttributes(gl, programPhysics, positionBuffer);
    twgl.setUniforms(programPhysics, {
      u_texture: fb1.attachments[0],
      gravity_center: mousepos,
      off_gravity: offGravity,
      restore_colors: restoreColors,
      dt: 2.5 * dt,
    });
    twgl.bindFramebufferInfo(gl, fb2);
    twgl.drawBufferInfo(gl, positionBuffer, gl.TRIANGLE_FAN);

    // drawing particles
    gl.useProgram(programDraw.program);
    twgl.setBuffersAndAttributes(gl, programDraw, pointsBuffer);
    twgl.setUniforms(programDraw, { u_texture: fb2.attachments[0] });
    twgl.bindFramebufferInfo(gl, null);
    twgl.drawBufferInfo(gl, pointsBuffer, gl.POINTS);

    // ping-pong buffers
    temp = fb1;
    fb1 = fb2;
    fb2 = temp;
  }

  (function animate(now) {
    draw(now / 1000);
    requestAnimationFrame(animate);
  })(0);

  function setMousePos(e) {
    mousepos[0] = e.clientX / gl.canvas.clientWidth;
    mousepos[1] = 1 - e.clientY / gl.canvas.clientHeight;
  }

  canvas.addEventListener('mousemove', setMousePos);

  canvas.addEventListener('mouseleave', () => {
    mousepos[0] = 0.5;
    mousepos[1] = 0.5;
  });
  
  canvas.addEventListener('mousedown', e => {
    if (e.button === 0) {
      offGravity = 1;
    } else {
      restoreColors = 1;
    }
  });

  window.addEventListener('mouseup', () => {
    offGravity = 0;
    restoreColors = 0;
  });

  function handleTouch(e) {
    e.preventDefault();
    setMousePos(e.touches[0]);
  }

  canvas.addEventListener('contextmenu', e => e.preventDefault());
  canvas.addEventListener('touchstart', handleTouch, {passive: false});
  canvas.addEventListener('touchmove', handleTouch, {passive: false});

</script>

</body>
</html>